package cz.possoft.explorer.converter;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;

import cz.possoft.explorer.ServiceFactory;

/**
 * @author <a href="mailto:mposolda@gmail.com">Marek Posolda</a>
 * @version $Revision$
 */
public class ConverterImpl implements Converter
{
   
   // helper field which contain all classes, which can be converted from String by ConverterProvider
   private Collection<Class<?>> stringConverterTypes;
   
   /**
    * This implementation is using ConverterProvider and it delegates to concrete method in converter provider to convert.
    */
   public <F,T> T convert(F input, Class<T> outputClazz) throws ConverterException
   {
      Class<F> inputClazz = (Class<F>)input.getClass();
      ConverterProvider converterProvider = ServiceFactory.getInstance().getConverterProvider();
      
      // now we need to find correct converter method
      Method converterMethod = findConverterMethod(converterProvider, inputClazz,outputClazz);
      if (converterMethod == null)
      {
         throw new ConverterException("Can't find converter method in ConverterProvider class " + converterProvider.getClass() + 
               " where input type is " + inputClazz + " and output type is " + outputClazz);
      }
            
      try
      {
         return (T)converterMethod.invoke(converterProvider, input, createNullObject(outputClazz));
      }
      catch (InvocationTargetException ite)
      {
         if ((ite.getCause() != null) && (ite.getCause() instanceof ConverterException))
         {
            throw (ConverterException)ite.getCause();
         }
         else
         {
            throw new ConverterException(ite);
         }
      }
      catch (Exception e)
      {
         throw new ConverterException(e);
      }           
   }
   
   private <F,T> Method findConverterMethod(ConverterProvider provider, Class<F> inputClass, Class<T> outputClass)
   {
      Class<? extends ConverterProvider> providerClass = provider.getClass();
      Class<? super F> currentInputClass = inputClass;

      Method m = null;
      try
      {
         // try to find method for our class
         m = providerClass.getMethod("convert", currentInputClass, outputClass);
      }
      catch (NoSuchMethodException nsme)
      {
         // try to find method for superclass
         if (inputClass.getSuperclass() != null)
         {
            m = findConverterMethod(provider, currentInputClass.getSuperclass(), outputClass);
         }
         
         // try to find method for interfaces
         if ((m == null) && (inputClass.getInterfaces() != null))
         {
            for (Class<?> clazz : inputClass.getInterfaces())
            {
               m = findConverterMethod(provider, clazz, outputClass);
               if (m != null)
               {
                  break;
               }
            }
         }
      }
      
      return m;
   }
   
   /**
    * get all types from ConverterProvider class, which can be converted from String
    * 
    * @return stringConverterTypes
    */
   public Collection<Class<?>> getStringConverterTypes()
   {
      if (stringConverterTypes == null)
      {
         stringConverterTypes = new HashSet<Class<?>>();
         ConverterProvider converterProvider = ServiceFactory.getInstance().getConverterProvider();
         Class<? extends ConverterProvider> providerClass = converterProvider.getClass();
         
         Method[] convertMethods = providerClass.getMethods();
         for (Method m : convertMethods)
         {
            if ((m.getName().equals("convert")) && (m.getParameterTypes() != null) &&
                  (m.getParameterTypes().length == 2) && (m.getParameterTypes()[0].equals(String.class)))
            {
               stringConverterTypes.add(m.getParameterTypes()[1]);
            }
         }         
      }
      return stringConverterTypes;
   }
   
   /**
    * We need to reset stringCoverterTypes when we replacing converter provider with new converter provider.
    * TODO: thread-safety
    *
    */
   public void resetStringConverterTypes()
   {
      stringConverterTypes = null;
   }

   /**
    * Helper method for injecting type into ConverterProviderImpl method. We can return "null" for almost all objects
    * , only exceptions are primitive types. 
    * 
    * @param <T>
    * @param clazz
    * @return some object if clazz is primitive type or return null if clazz is not primitive type.
    */
   private Object createNullObject(Class<?> clazz)
   {
      if (clazz.equals(int.class))
      {
         return 0;
      }
      else if (clazz.equals(long.class))
      {
         return 0l;
      }      
      else if (clazz.equals(char.class))
      {
         return 'c';
      }  
      else if (clazz.equals(boolean.class))
      {
         return true;
      }  
      else if (clazz.equals(double.class))
      {
         return 0d;
      }        
      else
      {
         return clazz.cast(null);
      }
   }
}

